测试代码与环境

环境:virtual studio 2022

编译目标:x86

测试代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#include <iostream>
#include <Windows.h>

void printHello() {
std::cout << "Hello World" << std::endl;
}

void hookedHello() {
std::cout << "Bye World" << std::endl;
}

void installHook() {
unsigned char jmp_code[5] = { 0 };
jmp_code[0] = 0xE9;
int offset = (int)hookedHello - ((int)printHello + 5);
*(int*)&jmp_code[1] = offset;
DWORD oldProtect = 0;
VirtualProtect(printHello, 4096, PAGE_EXECUTE_READWRITE, &oldProtect);
memcpy(printHello, jmp_code, 5);
}

int main() {
installHook();
printHello();
return 0;
}

编码思路

按照我从字面上的理解,原本的程序执行是一条拉直的线,而Hook的中文是钩子,通过钩子就可以将程序的线给拉歪,使其部分发生变动,但不影响其整体走向。

Inline Hook(内联钩子)则是一种用于修改或监视程序执行流程的技术。它通过修改目标函数的二进制代码,将目标函数的一部分或全部替换为跳转指令,从而实现在目标函数执行之前或之后注入自定义的代码。内联钩子通常用于实现函数的跟踪、监控、调试、破解等场景。

内联钩子的实现原理一般分为三个步骤:

  1. 定位目标函数:通过符号、地址、函数名等方式定位目标函数的地址。
  2. 修改目标函数的二进制代码:将目标函数的指令序列中的一部分或全部替换为跳转指令(通常是 jmpcall 指令),跳转到钩子函数中执行。
  3. 编写钩子函数:编写用于替换目标函数指令的自定义代码,通常会在其中执行一些操作,然后再跳转回原始的目标函数继续执行。

代码解析

image-20240329162240464

可以看到,虽然我们调用的是printHello(),但是因为我们的钩子,程序实际执行时调用了hookedHello()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void installHook() {
// 0xE9是x86架构中的相对跳转指令的操作码。用于将程序的控制流程转移到距离当前指令地址偏移量所指定的位置。这个偏移量是一个有符号的32位整数,因此,0xE9的的指令长度是1字节,加上偏移量的4字节,共5字节。
unsigned char jmp_code[5] = { 0 };
// 第一个字节置为jmp
jmp_code[0] = 0xE9;
// 计算相对偏移
int offset = (int)hookedHello - ((int)printHello + 5);
// 将jmp_code[1]所在地址转化为int型指针,并通过解引用将偏移赋值给该地址
*(int*)&jmp_code[1] = offset;
DWORD oldProtect = 0;
// 为printHello所在内存页赋予写权限
VirtualProtect(printHello, 4096, PAGE_EXECUTE_READWRITE, &oldProtect);
// 替换printHello前五个字节
memcpy(printHello, jmp_code, 5);
}

这样,当程序执行到 printHello 函数时,实际上会跳转到 hookedHello 函数执行,从而实现了对 printHello 函数的hook

将断点断在内存拷贝前

image-20240329171800849

步过

image-20240329172015535

可以看到,相同地址的汇编指令发生了变化。HOOK成功。

接下来HOOK一个Windows API,时间关系今天先写这些。